package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;
@Component
@Slf4j
public class CacheClient {   //缓存工具类
    private final StringRedisTemplate stringRedisTemplate;

    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);



    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private boolean unlock(String key){
        Boolean flag = stringRedisTemplate.delete(key);
        return BooleanUtil.isTrue(flag);
    }

    public  void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    public  void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        //设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));

        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit) { //使用缓存空值的方法解决缓存穿透的问题
        String key = keyPrefix + id;
        //1.从redis读取商铺缓存数据
        String data = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否为空
        if (StringUtils.isNotBlank(data)) {   //过滤null值和空白值
            //3.如果不为空，则返回商铺数据,将json格式转化为bean对象再返回
            return JSONUtil.toBean(data, type);
        }
        //如果为空，判断是缓存的空值，还是缓存未命中返回的null值
        if (data != null){
            //命中的是缓存的空值，返回给前端店铺不存在的信息
            return null;
        }
        //4.缓存为空，去数据库查数据
        R r = dbFallback.apply(id);
        //5.判断商铺是否存在
        if (r == null){
            //6.不存在返回错误给前端     不存在则缓存空值到redis 设置较短的过期时间 返回错误信息
//            return Result.fail("商铺不存在");
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        //7.存在，返回商铺数据，并将数据写入redis缓存,设置过期时间
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(r),time, unit);
        return r;
    }

    public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time,TimeUnit unit) { //使用逻辑过期时间解决缓存击穿（热点key问题）
        String key = keyPrefix + id;
        //1.从redis读取商铺缓存数据
        String data = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否为空
        if (StringUtils.isBlank(data)) {   //过滤null值和空白值
            //由于热点key的redis缓存都是预热好的，所以默认缓存都会命中，如果没有命中就是不存在
            return null;
        }

        //3.缓存命中，反序列化对象 判断缓存是否过期 逻辑过期时间
        RedisData redisData = JSONUtil.toBean(data, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        R r = JSONUtil.toBean((JSONObject) redisData.getData(),type);

        if (expireTime.isAfter(LocalDateTime.now())){
            //未过期，直接返回缓存数据
            return r;
        }

        //4.缓存过期 进行缓存重建  先要获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if (isLock){    // 注意成功获得锁后，应该进行二次检测，redis缓存是否过期，防止无效重建缓存，若未过期则可以不重建
            //获得互斥锁 开启新线程 进行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存  查数据库 设置逻辑过期时间 序列化为json 写入redis
                    R r1 = dbFallback.apply(id);
                    setWithLogicalExpire(key,r1,time,unit);



                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    unlock(lockKey);
                }
            });

        }

        //5.未获得锁 返回旧数据
        return r;
    }
}
